Skip to content

feat(slack): native OAuth trigger#5323

Open
TheodoreSpeaks wants to merge 12 commits into
stagingfrom
improve/slack
Open

feat(slack): native OAuth trigger#5323
TheodoreSpeaks wants to merge 12 commits into
stagingfrom
improve/slack

Conversation

@TheodoreSpeaks

@TheodoreSpeaks TheodoreSpeaks commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Redesigns the native Slack trigger around a single event per block with contextual filters, replacing the multi-select "Operations" model.

  • One trigger = one event (single-select eventType); compose complex behavior with multiple blocks instead of a downstream condition block
  • Full event catalog (SLACK_EVENT_CATALOG) as the single source of truth — message, app mentioned, reactions, message edited/deleted, file shared, member/channel lifecycle, pins, workspace join, app home, assistant — driving UI gating and route filtering
  • Manifest gating per app type: Sim mode offers only events the official app subscribes to; Custom mode offers all and generates the manifest (capabilities.ts). Deploy rejects an unsupported event on Sim mode
  • Contextual filters, each gated by event: Source (multiselect: DM / public / private, empty = any), Channels (picker + manual IDs), three-way Threads (messages+replies / messages only / replies only), Emoji, Name-contains
  • Correctness: always-on self-drop (app_id for messages, stored bot_user_id for reactions) with an advanced opt-in; "Ignore bot messages" = other bots only; Slack Connect routing via authorizations[]; message edit/delete subtypes mapped; DMs bypass the channel filter; pins:read added
  • Advanced-toggle support for standalone trigger-advanced fields (generalizes the block-level advanced section; zero blast radius — every existing such field is canonical-paired)
  • Adds Slack tools to the minimal dev registry so the action block runs end-to-end locally

Type of Change

  • New feature

Testing

  • Route filter unit tests (route.test.ts): source/threads/emoji/channel gating, self-drop, subtype mapping, legacy back-compat, DM-bypasses-channel-filter — all passing
  • bunx tsc --noEmit clean, bun run lint:check clean, bun run check:api-validation:strict passed
  • Manually verified end-to-end against a live workspace (event routed → filtered → action fired)

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

TheodoreSpeaks and others added 4 commits June 30, 2026 19:59
Add assistant:write, app_mentions:read, and im:history to the Slack bot
OAuth scopes so the Set Assistant Status / Title / Suggested Prompts tools
(assistant.threads.*) work with users' existing Slack credentials — no new
app or credentials required. Restore the action_assistant trigger capability
(scope assistant:write) in the manifest generator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpeT8J5yVCrrNQB9Hzm9uS
@vercel

vercel Bot commented Jul 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment Jul 7, 2026 3:51am

Request Review

@cursor

cursor Bot commented Jul 1, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Touches webhook ingestion (HMAC, multi-tenant routing), deploy/credential lifecycle, and a schema migration on webhook; incorrect routing or filtering could miss or mis-deliver events across workspaces.

Overview
Introduces a unified slack_oauth trigger (replacing slack_webhook on the Slack block) with Sim vs Custom app types: Sim uses OAuth + a shared /api/webhooks/slack ingest verified by SLACK_SIGNING_SECRET, routes by Slack team_id stored as webhook routing_key (nullable path), and fans out to all matching workflows; Custom keeps per-workflow URL + signing secret.

Trigger model is redesigned around one event per block (SLACK_EVENT_CATALOG), contextual filters (source, channels, threads, emoji, name), shared shouldSkipSlackTriggerEvent on native and custom paths, deploy-time auth.test team resolution, Sim-only event allowlist, and credential disconnect deactivating slack_app webhooks.

Editor changes treat trigger-advanced like standalone advanced fields; OAuth channel pickers can use triggerCredentials. OAuth scopes expand; Slack block/tools join minimal dev registries; tests cover the new route and filter behavior.

Reviewed by Cursor Bugbot for commit 4692708. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread apps/sim/app/api/webhooks/slack/route.ts Outdated
@greptile-apps

greptile-apps Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Redesigns the Sim native Slack trigger from a multi-select "operations" model to a single-event-per-block model backed by a unified event catalog (SLACK_EVENT_CATALOG), a new shared ingest endpoint (/api/webhooks/slack), and tenant routing by Slack team_id stored as webhook.routingKey rather than a per-workflow URL path.

  • New native OAuth path: a slack_app webhook is deployed with the workspace's team_id as its routing key (derived server-side via auth.test); inbound events arrive at a single HMAC-verified endpoint and fan out to all matching slack_app webhooks.
  • Contextual filters (source, channels, threads, emoji, name-contains) are catalog-driven so the trigger UI and the ingest route share one source of truth; self-drop and bot-filter logic is applied unconditionally on the shared path.
  • Correctness fixes included: edit/delete events are no longer dropped when channel_type is absent, the bot token for native reactions/files is resolved via the credential owner rather than the execution actor, and credential disconnection deactivates associated slack_app webhooks.

Confidence Score: 4/5

Safe to merge for all event types except channel_rename with a channel filter configured — those events will be silently dropped until the fix is applied.

The channel_rename event is the only case where Slack sends channel as an object rather than a string. resolveSlackEventChannel returns undefined for it, and because the catalog marks channel_rename with filters: ['channels'], the channel-scoped guard then drops every rename event the moment any channel filter is set. The rest of the redesign — routing, filtering, token resolution, credential cleanup, schema migration — is solid and well-tested.

apps/sim/lib/webhooks/providers/slack.ts — the resolveSlackEventChannel function needs to handle event.channel as an object for the channel_rename case.

Important Files Changed

Filename Overview
apps/sim/app/api/webhooks/slack/route.ts New shared-endpoint ingest route for the official Sim Slack app — verifies HMAC signature, routes by team_id (including Slack Connect), deduplicates overlapping webhook matches, and delegates to shouldSkipSlackTriggerEvent before queuing execution.
apps/sim/triggers/slack/shared.ts Single source of truth for the full Slack event catalog, filter gating, and shared trigger output schema. Clean catalog-driven UI/route consistency.
apps/sim/triggers/slack/oauth.ts New unified trigger config (sim/custom dual mode) with catalog-derived contextual filter sub-blocks and OAuth credential integration for the Sim path.
apps/sim/lib/webhooks/providers/slack.ts Updated provider handler adds shouldSkipSlackTriggerEvent for contextual filtering and fixes token resolution via credential owner, but resolveSlackEventChannel misses the channel_rename object-channel case, silently dropping all channel_rename events when any channel filter is set.
apps/sim/lib/webhooks/deploy.ts Deploy logic correctly branches on appType: Sim mode resolves team_id via auth.test and stores it as routingKey (never user input), custom mode reuses the legacy slack path. Sim-mode event gating against SIM_SUBSCRIBED_EVENTS is robust.
packages/db/schema.ts Drops NOT NULL on path and adds nullable routing_key column with a partial index — schema matches migration; existing path-based webhooks are unaffected.
packages/db/migrations/0256_slack_native_routing.sql Correct pattern: drops NOT NULL, adds column, COMMIT to exit transaction, then CREATE INDEX CONCURRENTLY to avoid table lock.
apps/sim/lib/webhooks/processor.ts Adds findWebhooksByRoutingKey for team-id-based fan-out and patches path ?? '' null-safety for polled events; both changes are correct.
apps/sim/lib/credentials/deletion.ts Adds deactivateSlackAppWebhooks to credential cleanup so routing stops on disconnect; uses parameterized JSON path operator to avoid injection.
apps/sim/app/api/webhooks/slack/route.test.ts Covers routing, skip-filter, Slack Connect dedup, and missing team_id path.
apps/sim/lib/webhooks/providers/slack.test.ts New shouldSkipSlackTriggerEvent tests cover source, threads, emoji, channel, self-drop, bot-filter, legacy back-compat, and the message_changed/missing channel_type fix.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Slack
    participant Ingest as /api/webhooks/slack
    participant DB as Database
    participant Filter as shouldSkipSlackTriggerEvent
    participant Queue as queueWebhookExecution

    Slack->>Ingest: POST (HMAC-signed event)
    Ingest->>Ingest: verifySlackRequestSignature
    Ingest->>Ingest: extract team_id + authorizations[].team_id
    Ingest->>DB: findWebhooksByRoutingKey(team_id) per workspace
    DB-->>Ingest: matching slack_app webhooks (deduplicated)
    loop for each webhook
        Ingest->>Filter: shouldSkipSlackTriggerEvent(payload, providerConfig)
        Filter-->>Ingest: "skip=true/false"
        alt "skip=false"
            Ingest->>DB: blockExistsInDeployment check
            Ingest->>Queue: queueWebhookExecution
        end
    end
    Ingest-->>Slack: HTTP 200

    note over DB,Queue: formatInput (later, async)
    Queue->>DB: resolve credentialId to account owner
    Queue->>Slack: auth.test / reactions.get / files download (if needed)
    Queue-->>Queue: SlackTriggerEvent output
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Slack
    participant Ingest as /api/webhooks/slack
    participant DB as Database
    participant Filter as shouldSkipSlackTriggerEvent
    participant Queue as queueWebhookExecution

    Slack->>Ingest: POST (HMAC-signed event)
    Ingest->>Ingest: verifySlackRequestSignature
    Ingest->>Ingest: extract team_id + authorizations[].team_id
    Ingest->>DB: findWebhooksByRoutingKey(team_id) per workspace
    DB-->>Ingest: matching slack_app webhooks (deduplicated)
    loop for each webhook
        Ingest->>Filter: shouldSkipSlackTriggerEvent(payload, providerConfig)
        Filter-->>Ingest: "skip=true/false"
        alt "skip=false"
            Ingest->>DB: blockExistsInDeployment check
            Ingest->>Queue: queueWebhookExecution
        end
    end
    Ingest-->>Slack: HTTP 200

    note over DB,Queue: formatInput (later, async)
    Queue->>DB: resolve credentialId to account owner
    Queue->>Slack: auth.test / reactions.get / files download (if needed)
    Queue-->>Queue: SlackTriggerEvent output
Loading

Reviews (7): Last reviewed commit: "Merge remote-tracking branch 'origin/sta..." | Re-trigger Greptile

Comment thread apps/sim/app/api/webhooks/slack/route.ts Outdated
Comment thread apps/sim/lib/webhooks/deploy.ts
Comment thread apps/sim/triggers/slack/oauth.ts
# Conflicts:
#	apps/sim/blocks/blocks/slack.ts
#	apps/sim/lib/webhooks/deploy.ts
#	packages/db/migrations/meta/0253_snapshot.json
#	packages/db/migrations/meta/_journal.json
#	scripts/check-api-validation-contracts.ts
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review — addressed the three findings: (1) unmapped/unselected events now drop instead of bypassing the Operations filter, (2) formatInput resolves the OAuth bot token for slack_app webhooks so reaction-message text works, (3) re-added the includeFiles toggle + token resolution so native file downloads work.

Comment thread apps/sim/app/api/webhooks/slack/route.ts Outdated
Comment thread apps/sim/lib/webhooks/providers/slack.ts Outdated
Comment thread apps/sim/app/api/webhooks/slack/route.ts Outdated
…token via credential owner not execution actor
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review — addressed both: empty operation selection now fires nothing (no match-all), and the OAuth token is resolved via the credential owner (not the execution actor) so it works in shared workspaces.

Comment thread apps/sim/app/api/webhooks/slack/route.ts Outdated
Comment thread apps/sim/app/api/webhooks/slack/route.ts Outdated
Comment thread apps/sim/lib/webhooks/deploy.ts Outdated
Comment thread apps/sim/app/api/webhooks/slack/route.ts
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

error: { message: 'Select a Slack account for the trigger.', status: 400 },
}
}
const botToken = await refreshAccessTokenIfNeeded(credentialId, userId, requestId)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deploy uses deployer not owner

High Severity

Sim-mode Slack deploy resolves team_id via refreshAccessTokenIfNeeded using the deploying user’s id, but that helper only loads OAuth tokens when the account row belongs to that user. In shared workspaces, a teammate deploying a trigger wired to someone else’s Slack credential gets a failed deploy even though runtime formatInput already resolves the credential owner for the same case.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 65c1415. Configure here.

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

Fixed: custom (bring-your-own-app) Slack deliveries now apply the same event/source/threads/emoji/channel/bot filtering as the native app. Extracted the filter into a shared shouldSkipSlackTriggerEvent used by both /api/webhooks/slack and the path route via slackHandler.shouldSkipEvent (guarded to slack_oauth so the legacy slack_webhook trigger stays unfiltered).

@greptile review

Comment thread apps/sim/lib/webhooks/providers/slack.ts
# Conflicts:
#	apps/sim/lib/webhooks/deploy.ts
#	apps/sim/lib/webhooks/utils.server.ts
#	packages/db/migrations/meta/0254_snapshot.json
#	packages/db/migrations/meta/_journal.json
#	packages/db/schema.ts
#	scripts/check-api-validation-contracts.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4692708. Configure here.

effectiveProvider = 'slack_app'
effectivePath = null
} else {
effectiveProvider = 'slack'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Custom reactions lack self-drop

Medium Severity

For slack_oauth custom app deploys, bot_user_id is never written to providerConfig, while Sim mode sets it via auth.test. Shared filtering treats reaction self-drop only when bot_user_id matches event.user, so a custom reaction trigger can fire on the bot’s own reactions and risk runaway workflows.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4692708. Configure here.

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

Rebased onto staging (resolved the 0255_remove_credential_sets conflicts; renumbered the slack routing-key migration 0254 → 0256, kept it CONCURRENTLY). Also fixed the Source filter drops edit events finding: message_changed/message_deleted often omit top-level channel_type, so the Source filter now only drops when the type is actually known — a missing type no longer swallows every edit/delete.

@greptile review

Comment on lines +477 to +485
export function resolveSlackEventChannel(
event: Record<string, unknown> | undefined
): string | undefined {
if (!event) return undefined
if (typeof event.channel === 'string') return event.channel
const item = event.item as Record<string, unknown> | undefined
if (typeof item?.channel === 'string') return item.channel
return typeof event.channel_id === 'string' ? event.channel_id : undefined
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 For channel_rename events, Slack sends channel as an object {"id":"C024BE91L","name":"newname",...} rather than a bare string. resolveSlackEventChannel only checks typeof event.channel === 'string', so it returns undefined for every rename payload. Because channel_rename is marked filters: ['channels'] in the catalog, channelScoped evaluates to true whenever a channel filter is configured. The guard !eventChannel || !selectedChannels.includes(eventChannel) then fires on every channel_rename event, silently dropping all of them regardless of which channel the user selected. Adding an object branch before the item.channel check fixes it with no impact on other event types.

Suggested change
export function resolveSlackEventChannel(
event: Record<string, unknown> | undefined
): string | undefined {
if (!event) return undefined
if (typeof event.channel === 'string') return event.channel
const item = event.item as Record<string, unknown> | undefined
if (typeof item?.channel === 'string') return item.channel
return typeof event.channel_id === 'string' ? event.channel_id : undefined
}
export function resolveSlackEventChannel(
event: Record<string, unknown> | undefined
): string | undefined {
if (!event) return undefined
if (typeof event.channel === 'string') return event.channel
// channel_rename sends channel as an object {id, name, ...} — extract the id.
const channelObj = event.channel as Record<string, unknown> | undefined
if (channelObj && typeof channelObj.id === 'string') return channelObj.id
const item = event.item as Record<string, unknown> | undefined
if (typeof item?.channel === 'string') return item.channel
return typeof event.channel_id === 'string' ? event.channel_id : undefined
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant